Starting with iOS 3.2, UIViews
can handle not only individual touch events, but they can also look for
particular kinds of touch actions and let your code know when they
occur. Some of this isn't entirely new. UIScrollView,
for instance, has always known how to watch for pinch and drag
gestures, which it uses for controlling zoom levels and panning the
view. What's new is that you can now tell any UIView to watch for specific gestures and let you know when they occur.
To make this work, you first create an instance of the new UIGestureRecognizer class, or rather, an instance of one of its many subclasses:
Each of those is
fine-tuned to detect a particular user gesture, clearly indicated in
the class name. Most of them have at least one property that allows you
to set some configuration options or read a value back.
After creating a gesture recognizer, you just pass it to a view using the addGestureRecognizer:
method. Then the method you specified when creating the gesture
recognizer will be called whenever the user performs that gesture.
Let's put this into action using Dudel.
1. Adding Undo to Dudel
One key feature that Dudel is
missing is any sort of undo action. Each stroke you make in a drawing
is a part of your drawing forever. We're going to remedy that by
assigning a gesture to open a small popover containing a single item
that lets us remove the most recently created Drawable object in dudelView's array. It's going to end up looking like Figure 1.
Then open the Xcode project in the new
directory.
Let's start by making the view controller that will display a small pop-up menu in Dudel. Create a new UIViewController subclass, this time as a subclass of UITableViewController, without a matching .xib file, and name it DudelEditController. Here's the entire content of both the .h and .m files:
// DudelEditController.h
#import <UIKit/UIKit.h>
#define DudelEditControllerDelete @"DudelEditControllerDelete"
@interface DudelEditController : UITableViewController {
UIPopoverController *container;
}
@property (assign, nonatomic) UIPopoverController *container;
@end
// DudelEditController.m
#import "DudelEditController.h"
@implementation DudelEditController
@synthesize container;
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation {
// Override to allow orientations other than the default portrait orientation.
return YES;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)s {
// Return the number of rows in the section.
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
}
cell.textLabel.text = @"Delete last object";
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)ip {
[[NSNotificationCenter defaultCenter] postNotificationName:DudelEditControllerDelete
object:self];
}
@end
Now open DudelViewController.m and add this line near the top of the file:
#import "DudelEditController.h"
Then add these lines to the end of viewDidLoad:
UILongPressGestureRecognizer *longPress =
[[[UILongPressGestureRecognizer alloc] initWithTarget:self
action:@selector(handleLongPress:)] autorelease];
[dudelView addGestureRecognizer:longPress];
Next, implement the handleLongPress: method referenced earlier.
- (void)handleLongPress:(UIGestureRecognizer *)gr {
if (gr.state == UIGestureRecognizerStateBegan) {
DudelEditController *c = [[[DudelEditController alloc]
initWithStyle:UITableViewStylePlain] autorelease];
[self setupNewPopoverControllerForViewController:c];
self.currentPopover.popoverContentSize = CGSizeMake(320, 44*1);
c.container = self.currentPopover;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(dudelEditControllerSelectedDelete:)
name:DudelEditControllerDelete object:c];
CGRect popoverRect = CGRectZero;
popoverRect.origin = [gr locationInView:dudelView];
[self.currentPopover presentPopoverFromRect:popoverRect inView:dudelView
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
}
Now it's time to implement the method that is called when the menu item is actually selected.
- (void)dudelEditControllerSelectedDelete:(NSNotification *)n {
DudelEditController *c = [n object];
UIPopoverController *popoverController = c.container;
[popoverController dismissPopoverAnimated:YES];
[self handleDismissedPopoverController:popoverController];
self.currentPopover = nil;
if ([dudelView.drawables count] > 0) {
[dudelView.drawables removeLastObject];
[dudelView setNeedsDisplay];
}
}
Build and run your app, do
some doodling, and then press and hold anywhere on the screen until the
popover appears. Select its one item, and watch as the last shape or
stroke you made suddenly disappears! This is great, but now try to draw
a couple more shapes using whatever tool you already had selected.
You'll see that things get a little screwy. The screen doesn't seem to
update properly while you drag, and the first shape you draw will
disappear when you start drawing the next one. This is all due to the
gesture activity leaving the chosen tool in an inconsistent state,
which is easily remedied.
2. Resetting the Selected Tool's State
We need to make sure that
when active touches are canceled (which happens when the gesture
recognizer decides that a gesture is happening), the selected tool's
state is reset so that it doesn't think it's still in the middle of
tracking a drag.
Open the .m file for each of the tool classes (except FreehandTool), and add the following line to the touchesCancelled:withEvent: method:
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
// If you already had any other code here, leave it alone and add this:
[self deactivate];
}
The FreehandTool class is the exception. It already implements this method. Here, just add a single line:
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[self activate];
isDragging = NO;
}
This one is a little different from the others. It calls the activate method instead of deactivate. That's due to a peculiarity in the creation of this class, where deactivate finishes the current drawing action—just what we want to avoid!
Now you should be able to build and run the app, and see everything behaving as it should.
We've used only UILongPressGestureRecognizer for this example, but the other gesture recognizers work similarly.